/*

Copyright (C) 2000 - 2008 Christian Kreibich <christian@whoop.org>.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies of the Software and its documentation and acknowledgment shall be
given in the documentation and software packages that this Software was
used.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

*/
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <ctype.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include <nd_macros.h>
#include <nd_debug.h>
#include <support.h>
#include <gtkhex.h>

enum {
  CHANGED,
  MOVE_CURSOR,
  SELECTION,
  LAST_SIGNAL
};

static GtkVBoxClass *parent_class = NULL;
static guint hex_signals[LAST_SIGNAL] = { 0 };

struct _GtkHexPrivate
{
  gchar   *null_map;
  int    null_map_len;
  int    null_map_used;
};

/* The width of a line of text in the hex-mode view, consisting
 * of offset, hex view and ascii view:
 *
 * 32 +     16 characters per 8 Bytes, twice
 * (2*7) +  Single space between bytes, twice
 * 4 +      Two spaces between 8-byte sets and ascii
 * 1 +      For newline
 * 17 +     For ascii display, with spacer column
 * 6        For 5-digit offset counter, including spacer
 */ 
#define HEX_LINE_WIDTH               74

#define HEX_LINE_START                6
#define HEX_LINE_END                 53
#define HEX_LINE_START_ASCII         56
#define HEX_LINE_START_RIGHT_ASCII   65
#define HEX_LINE_LEFT_MIDDLE         28
#define HEX_LINE_RIGHT_MIDDLE        31
#define HEX_BLOCK_LEN                23

#define HEX_LINE_BYTES               16

#define NULL_CHAR                    0xF8
#define NONPRINT_CHAR                0xB7

static int       hex_get_nowhite_index(GtkHex *hex, int index);
static gboolean  hex_cursor_over_hex(GtkHex *hex);
static gboolean  hex_cursor_over_hex_ascii(GtkHex *hex);
static gboolean  hex_cursor_over_high_nibble(GtkHex *hex);
static int       hex_cursor_index_to_byte_index(GtkHex *hex);
static void      hex_byte_index_to_cursor_indices(int byte_index, int *i1, int *i2);
static void      hex_sync_data_to_ascii(GtkHex *hex);
static void      hex_text_ascii_colorify(GtkHex *hex, guint start, int length);
static void      hex_text_set_cursor(GtkHex *hex, guint index);
static gboolean  hex_is_char_unprintable(guchar val);

/* Inserts text into null map */
static void
null_map_insert(GtkHexPrivate *priv, gchar *text,
		int pos, int length)
{
  if (priv->null_map_len - priv->null_map_used <= length)
    {
      priv->null_map = realloc(priv->null_map, 2 * priv->null_map_len);
      priv->null_map_len *= 2;
    }
  
  memmove(priv->null_map + pos + length, 
	  priv->null_map + pos,
	  priv->null_map_used - pos);
  memcpy(priv->null_map + pos, text, length);
  priv->null_map_used += length;
}

/* Removes text from null map */
static void
null_map_delete(GtkHexPrivate *priv, int start, int end)
{
  if (end < start)
    end = priv->null_map_used;
  
  memmove(priv->null_map + start,
	  priv->null_map + end,
	  priv->null_map_len - end);
  priv->null_map_used -= (end - start);
}


/* Sets the editor into hex mode
 */
static void
on_mode_hex_toggled             (GtkToggleButton *togglebutton,
				 gpointer         user_data)
{
  if (gtk_toggle_button_get_active(togglebutton))
    gtk_hex_set_mode((GtkHex*) user_data, GTK_RAW_HEX);
}


/* Sets the editor into ascii mode
 */
static void
on_mode_ascii_toggled           (GtkToggleButton *togglebutton,
				 gpointer         user_data)
{
  if (gtk_toggle_button_get_active(togglebutton))
    gtk_hex_set_mode((GtkHex*) user_data, GTK_RAW_ASCII);
}


/* We use the motion notify callback only to kill the object's
 * handling of text selection -- we use the selection as our
 * cursor, thus we want to disable it. This works through setting
 * the pressed button to zero, making the object's handler think
 * no button was pressed and changing nothing. This handler
 * thus must be called *before* the object's handler
 */
static gboolean
on_hex_text_motion_notify_event        (GtkWidget       *widget,
                                        GdkEventMotion  *event,
                                        gpointer         user_data)
{
  GtkHex *hex = GTK_HEX(user_data);

  if (hex->mode == GTK_RAW_HEX)
    GTK_TEXT(hex->hex_text)->button = 0;

  return FALSE;
  TOUCH(event);
  TOUCH(widget);
}


/* This handler takes care of updating the null
 * map when text is deleted.
 */
static gboolean
on_hex_text_deleted(GtkWidget       *widget,
		    gint             start,
		    gint             end,
		    gpointer         user_data)
{
  GtkHex *hex = GTK_HEX(user_data);

  if (hex->mode == GTK_RAW_ASCII)
    {
      null_map_delete(hex->priv, start, end);
      hex_text_ascii_colorify(hex, start, -ABS(end - start));
      hex_sync_data_to_ascii(hex);

      gtk_signal_emit(GTK_OBJECT(hex), hex_signals[CHANGED],
		      start, ABS(end - start));
    }

  return FALSE;
  TOUCH(widget);
}


/* This handles text insertions *BEFORE* the object's own
 * handler. We use it to update the contents of the null map.
 */
static gboolean
on_hex_text_inserted(GtkWidget       *widget,
		     const gchar     *text,
		     gint             length,
		     gint            *position,
		     gpointer         user_data)
{
  GtkHex  *hex = GTK_HEX(user_data);
  guint    pos;
  gint     i;

  if (hex->mode == GTK_RAW_ASCII)
    {
      gchar *whitespace;
      pos = *position;

      /* Update null_map to reflect changes: */

      if ( (whitespace = g_new0(gchar, length + 1)))
	{
	  memset(whitespace, ' ', length);

	  for (i = 0; i < length; i++)
	    {
	      if (hex_is_char_unprintable(text[i]))
		{
		  switch (text[i])
		    {
		    case '\n':
		      whitespace[i] = '\n';
		      break;
		      
		    case 0:
		      whitespace[i] = '0';
		      break;
		      
		    default:
		      whitespace[i] = text[i];
		    }	      
		}
	    }
	  
	  null_map_insert(hex->priv, whitespace, pos, length);
	  g_free(whitespace);
	}
    }

  return FALSE;
  TOUCH(widget);
  TOUCH(text);
}


/* This is the text insertion handler that runs *AFTER*
 * the object's own handler. We use it to cut excess data
 * at the end of the text buffers and to reflect the changes
 * in the packet's payload.
 */
static gboolean
on_hex_text_inserted_after(GtkWidget       *widget,
			   const gchar     *text,
			   gint             length,
			   gint            *position,
			   gpointer         user_data)
{
  GtkHex  *hex = GTK_HEX(user_data);

  if (hex->mode == GTK_RAW_ASCII)
    {

      /* Make sure text gets no longer than packet payload,
	 and reflect changes in the null map: */

      hex_text_ascii_colorify(hex, *position - length, ABS(length));
      hex_sync_data_to_ascii(hex);

      gtk_signal_emit(GTK_OBJECT(hex), hex_signals[CHANGED],
		      *position - length, length);
    }


  return FALSE;
  TOUCH(widget);
  TOUCH(text);
  TOUCH(length);
  TOUCH(position);
}


static gboolean
hex_is_char_unprintable(guchar val)
{
  return (val < 0x20 || (val >= 0x7f && val < 0xa0));
}


/* This function selects a character in the hex editor (thus placing
 * the cursor) and also takes care of hilighting the corresponding
 * character in the ascii or hex field
 */
static void
hex_text_set_cursor(GtkHex *hex, guint index)
{
  static guint old_index = 0, old_len = 0;

  GtkEditable *ed;
  gchar       *text;
  int          byte_offset, i1, i2;

  if (!hex || hex->mode != GTK_RAW_HEX)
    return;

  ed = GTK_EDITABLE(hex->hex_text);

  gtk_editable_select_region(ed, index, index + 1);

  gtk_text_freeze(GTK_TEXT(ed));
  gtk_signal_handler_block_by_data(GTK_OBJECT(hex->hex_text), hex);

  if (old_len)
    {
      text = gtk_editable_get_chars(ed, old_index, old_index + old_len);
      gtk_editable_delete_text(ed, old_index, old_index + old_len);
      gtk_text_set_point(GTK_TEXT(hex->hex_text), old_index);
      gtk_text_insert(GTK_TEXT(hex->hex_text), NULL, NULL, NULL, text, old_len);
      g_free(text);
    }

  gtk_signal_handler_unblock_by_data(GTK_OBJECT(hex->hex_text), hex);
  gtk_text_thaw(GTK_TEXT(ed));

  byte_offset = hex_cursor_index_to_byte_index(hex);
  hex_byte_index_to_cursor_indices(byte_offset, &i1, &i2);

  gtk_text_freeze(GTK_TEXT(ed));
  gtk_signal_handler_block_by_data(GTK_OBJECT(hex->hex_text), hex);

  if (hex_cursor_over_hex(hex))
    {
      text = gtk_editable_get_chars(ed, i2, i2 + 1);
      gtk_editable_delete_text(ed, i2, i2 + 1);
      gtk_text_set_point(GTK_TEXT(hex->hex_text), i2);
      gtk_text_insert(GTK_TEXT(hex->hex_text), NULL, NULL,
		      &GTK_WIDGET(hex->hex_text)->style->base[GTK_STATE_INSENSITIVE],
		      text, 1);
      g_free(text);      
      old_index = i2;
      old_len = 1;
    }
  else
    {
      text = gtk_editable_get_chars(ed, i1, i1 + 2);
      gtk_editable_delete_text(ed, i1, i1 + 2);
      gtk_text_set_point(GTK_TEXT(hex->hex_text), i1);
      gtk_text_insert(GTK_TEXT(hex->hex_text), NULL, NULL,
		      &GTK_WIDGET(hex->hex_text)->style->base[GTK_STATE_INSENSITIVE],
		      text, 2);
      g_free(text);      
      old_index = i1;
      old_len = 2;
    }

  gtk_signal_handler_unblock_by_data(GTK_OBJECT(hex->hex_text), hex);
  gtk_text_thaw(GTK_TEXT(ed));

  gtk_editable_select_region(ed, index, index + 1);
}


static void
hex_text_ascii_colorify(GtkHex *hex, guint start, int length)
{
  GtkText *hex_text;
  char *text;
  guint len, min_len, point;

  if (!hex || hex->mode != GTK_RAW_ASCII)
    return;

  hex_text = GTK_TEXT(hex->hex_text);

  gtk_signal_handler_block_by_data(GTK_OBJECT(hex->hex_text), hex);
  gtk_text_freeze(hex_text);

  len = gtk_text_get_length(hex_text);
  min_len = MIN(hex->data_size - 1, len);
  text = gtk_editable_get_chars(GTK_EDITABLE(hex_text), 0, -1);
  point = gtk_editable_get_position(GTK_EDITABLE(hex_text));

  if (length > 0)
    {
      if (len > hex->data_size)
	{
	  gtk_editable_delete_text(GTK_EDITABLE(hex_text), hex->data_size, -1);
	  gtk_text_set_point(hex_text, hex->data_size);
	  gtk_text_insert(hex_text, NULL,
			  &GTK_WIDGET(hex_text)->style->text[GTK_STATE_INSENSITIVE],
			  &GTK_WIDGET(hex_text)->style->base[GTK_STATE_NORMAL],
			  &text[hex->data_size],
			  len - hex->data_size);
	}
    }
  else
    {
      if (start >= hex->data_size)
	{
	  gtk_editable_delete_text(GTK_EDITABLE(hex_text), hex->data_size, -1);
	  gtk_text_set_point(hex_text, hex->data_size);
	  gtk_text_insert(hex_text, NULL,
			  &GTK_WIDGET(hex_text)->style->text[GTK_STATE_INSENSITIVE],
			  &GTK_WIDGET(hex_text)->style->base[GTK_STATE_NORMAL],
			  &text[hex->data_size],
			  len - hex->data_size);
	}
      else
	{
	  gtk_editable_delete_text(GTK_EDITABLE(hex_text), point, -1);
	  gtk_text_insert(hex_text, NULL, NULL, NULL, &text[point],
			  MIN(hex->data_size - point, len - point));
	  
	  if (len > hex->data_size)
	    {
	      gtk_text_set_point(hex_text, hex->data_size);
	      gtk_text_insert(hex_text, NULL,
			      &GTK_WIDGET(hex_text)->style->text[GTK_STATE_INSENSITIVE],
			      &GTK_WIDGET(hex_text)->style->base[GTK_STATE_NORMAL],
			      &text[hex->data_size],
			      len - hex->data_size);
	    }
	}
    }
  
  gtk_text_thaw(hex_text);
  gtk_signal_handler_unblock_by_data(GTK_OBJECT(hex->hex_text), hex);
  gtk_editable_set_position(GTK_EDITABLE(hex_text), point);

  g_free(text);
}


/* This function handles the case of cursor-left being
 * pressed while hex mode is active.
 */
static void
hex_handle_hex_left(GtkHex *hex)
{
  int line_offset, line_num;
  GtkEditable *ed;

  ed = GTK_EDITABLE(hex->hex_text);
  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;
  line_num = ed->selection_start_pos / HEX_LINE_WIDTH;

  /* If we're at the beginning of either hex or ascii data,
     don't do anything. */
  if ((ed->selection_start_pos == HEX_LINE_START_ASCII) ||
      (ed->selection_start_pos == HEX_LINE_START))
    return;

  /* Check for intervals in the hex display: */
  
  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_LEFT_MIDDLE)
    {
      if (line_offset == HEX_LINE_START)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 27);
	  return;
	}

      if ((line_offset - HEX_LINE_START) % 3 == 1)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 1);
	}
      else
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 2);
	}
      return;
    }

  if (line_offset >= HEX_LINE_RIGHT_MIDDLE && line_offset <= HEX_LINE_END)
    {
      if (line_offset == HEX_LINE_RIGHT_MIDDLE)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 3);
	  return;
	}

      if ((line_offset - HEX_LINE_LEFT_MIDDLE) % 3 == 1)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 1);
	}
      else
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos - 2);
	}
      return;
    }

  /* Still here? Ok, check for intervals in the ascii display */

  if (line_offset == HEX_LINE_START_RIGHT_ASCII)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos - 2);
      return;
    }

  if (line_offset > HEX_LINE_START_ASCII && line_offset <= HEX_LINE_START_RIGHT_ASCII)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos - 1);
      return;
    }

  if (line_offset >= HEX_LINE_START_RIGHT_ASCII && line_offset <= HEX_LINE_START_RIGHT_ASCII + 7)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos - 1);
      return;
    }

  if (line_offset == HEX_LINE_START_ASCII)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos - HEX_LINE_START_ASCII - 2);
      return;
    }
}


/* This function handles the case of cursor-right being
 * pressed while hex mode is active.
 */
static void
hex_handle_hex_right(GtkHex *hex)
{
  int line_offset, line_num;
  guint byte_index;
  GtkEditable *ed;

  ed = GTK_EDITABLE(hex->hex_text);

  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;
  line_num = ed->selection_start_pos / HEX_LINE_WIDTH;
  byte_index = hex_cursor_index_to_byte_index(hex);

  /* If we're at the last byte and are using hex display,
     it depends on whether we've selected the low or high nibble
     whether we can move any further to the right. */
  if (byte_index == hex->data_size - 1)
    {
      if (line_offset < HEX_LINE_RIGHT_MIDDLE)
	{
	  if ((line_offset - HEX_LINE_START) % 3 == 0)
	    hex_text_set_cursor(hex, ed->selection_start_pos + 1);
	}
      else if ((line_offset - HEX_LINE_RIGHT_MIDDLE) % 3 == 0)
	hex_text_set_cursor(hex, ed->selection_start_pos + 1);

      return;
    }

  /* Check for intervals in the hex display: */

  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_LEFT_MIDDLE)
    {
      if (line_offset == HEX_LINE_LEFT_MIDDLE)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 3);
	  return;
	}

      if ((line_offset - HEX_LINE_START) % 3 == 1)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 2);
	}
      else
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 1);
	}

      return;
    }

  if (line_offset >= HEX_LINE_RIGHT_MIDDLE && line_offset <= HEX_LINE_END)
    {
      if (line_offset == HEX_LINE_END)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 27);
	  return;
	}

      if ((line_offset - HEX_LINE_LEFT_MIDDLE) % 3 == 1)
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 2);
	}
      else
	{
	  hex_text_set_cursor(hex, ed->selection_start_pos + 1);
	}
      
      return;
    }

  /* Check for intervals in the ascii display: */

  if (line_offset == HEX_LINE_START_RIGHT_ASCII - 2)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos + 2);
      return;
    }

  if (line_offset >= HEX_LINE_START_ASCII && line_offset < HEX_LINE_START_RIGHT_ASCII)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos + 1);
      return;
    }

  if (line_offset >= HEX_LINE_START_RIGHT_ASCII && line_offset < HEX_LINE_START_RIGHT_ASCII + 7)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos + 1);
      return;
    }

  if (line_offset == HEX_LINE_START_RIGHT_ASCII + 7)
    {
      hex_text_set_cursor(hex, ed->selection_start_pos + HEX_LINE_START_ASCII + 2);
      return;
    }
}


static void
hex_update_current(GtkHex *hex, int byte_offset)
{
  GtkEditable *ed;
  int  old, old2, i1, i2;
  char s[16];

  if (!hex || hex->mode != GTK_RAW_HEX)
    return;

  ed = GTK_EDITABLE(hex->hex_text);

  hex_byte_index_to_cursor_indices(byte_offset, &i1, &i2);
  gtk_text_freeze(GTK_TEXT(hex->hex_text));
  old = old2 = ed->selection_start_pos;

  s[0] = hex->data[byte_offset];
  s[1] = 0;
  
  if (hex_is_char_unprintable(s[0]))
    {
      if (s[0] == 0)
	s[0] = NULL_CHAR;
      else
	s[0] = NONPRINT_CHAR;
    }

  gtk_editable_delete_text(ed, i2, i2 + 1);      
  gtk_editable_insert_text(ed, s, 1, &i2);
  
  g_snprintf(s, 16, "%.2x", hex->data[byte_offset]);
  gtk_editable_delete_text(ed, i1, i1 + 2);
  gtk_editable_insert_text(ed, s, 2, &i1);

  gtk_text_thaw(GTK_TEXT(hex->hex_text));
  hex_text_set_cursor(hex, old);
}


static gboolean
hex_text_cursor_key_press(GtkHex *hex, GdkEventKey *event, guint byte_offset)
{
  GtkEditable *ed;

  ed = GTK_EDITABLE(hex->hex_text);

  if (hex->mode == GTK_RAW_HEX)
    {
      switch(event->keyval)
	{
	case GDK_Up:
	  if ((int) ed->selection_start_pos - HEX_LINE_WIDTH > 0)
	    hex_text_set_cursor(hex, ed->selection_start_pos - HEX_LINE_WIDTH);
	  return TRUE;
	  break;
	  
	case GDK_Down:
	  if (byte_offset + 16 < hex->data_size)
	    hex_text_set_cursor(hex, ed->selection_start_pos + HEX_LINE_WIDTH);
	  return TRUE;
	  break;
	  
	case GDK_Left:
	  hex_handle_hex_left(hex);
	  return TRUE;
	  break;
	  
	case GDK_Right:
	  hex_handle_hex_right(hex);
	  return TRUE;
	  break;
	  
	case GDK_Return:
	  if (event->string)
	    event->string[0] = '\n';
	  break;
	}
    }

  return FALSE;
}


static gboolean
hex_text_data_key_press(GtkHex *hex, GdkEventKey *event, int byte_offset)
{
  char val = 0;

  if (!hex)
    return FALSE;

  if (hex->mode == GTK_RAW_HEX && event->string && strlen(event->string) == 1)
    {
      if (hex_cursor_over_hex(hex))
	{
	  /* Accept only hex characters when editing hex data */
	  if ((event->string[0] >= 'a' && event->string[0] <= 'f') ||
	      (event->string[0] >= 'A' && event->string[0] <= 'F'))
	    {
	      val = tolower(event->string[0]) - 87;
	    }
	  else if (event->string[0] >= '0' && event->string[0] <= '9')
	    {
	      val = event->string[0] - 48;
	    }
	  else
	    {
	      return FALSE;
	    }

	  if (hex_cursor_over_high_nibble(hex))
	    {
	      hex->data[byte_offset] &= 0x0F;
	      hex->data[byte_offset] |= (val << 4);
	    }
	  else
	    {
	      hex->data[byte_offset] &= 0xF0;
	      hex->data[byte_offset] |= val;
	    }

	  hex_update_current(hex, byte_offset);
	  hex_handle_hex_right(hex);
	}
      else if (hex_cursor_over_hex_ascii(hex))
	{
	  if (event->string[0] < 32 || event->string[0] > 126)
	    return FALSE;

	  hex->data[byte_offset] = event->string[0];

	  hex_update_current(hex, byte_offset);
	  hex_handle_hex_right(hex);
	}

      gtk_signal_emit(GTK_OBJECT(hex), hex_signals[CHANGED],
		      byte_offset, 1);
    }

  return FALSE;
}


static gboolean
on_hex_text_key_press_event        (GtkWidget       *widget,
				    GdkEventKey     *event,
				    gpointer         user_data)
{
  gboolean done;
  int byte_offset;
  GtkHex *hex;
  GtkEditable *ed;

  if (!user_data)
    return FALSE;

  hex = GTK_HEX(user_data);
  ed = GTK_EDITABLE(widget);

  /* If the user pressed Ctrl-C, copy the packet content as we
   * rendered it into the clipboard.
   */
  if ((event->state & GDK_CONTROL_MASK) != 0 &&
      event->keyval == GDK_c)
    {
      gchar *content = gtk_editable_get_chars(GTK_EDITABLE(hex->hex_text), 0, -1);
      gtk_signal_emit(GTK_OBJECT(hex), hex_signals[SELECTION],
		      content);
      g_free(content);
      gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), "key_press_event");
      return TRUE;
    }
      
  if (hex->mode != GTK_RAW_HEX)
    return FALSE;

  if (!ed->has_selection)
    return FALSE;

  byte_offset = hex_cursor_index_to_byte_index(hex);

  /* We'll first check for cursor navigation and delegate handling
   * to the appropriate handler routines.
   */
  done = hex_text_cursor_key_press(hex, event, byte_offset);

  if (!done)
    hex_text_data_key_press(hex, event, byte_offset);

  byte_offset = hex_cursor_index_to_byte_index(hex);
  gtk_signal_emit(GTK_OBJECT(hex), hex_signals[MOVE_CURSOR], byte_offset);

  gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), "key_press_event");

  return TRUE;
}


/* This is our callback to indicate cursor placement.
 * This is called after the object's handler, which
 * even in non-editable mode sets GtkText->current_pos */
gboolean
on_hex_button_press_event     (GtkWidget       *widget,
			       GdkEventButton  *event,
			       gpointer         user_data)
{
  int index;
  GtkHex *hex;
  GtkEditable *ed;

  ed = GTK_EDITABLE(widget);
  hex = GTK_HEX(user_data);
  
  if (hex->mode == GTK_RAW_ASCII)
    return FALSE;

  index = hex_get_nowhite_index(hex, ed->current_pos);
  hex_text_set_cursor(hex, index);

  return FALSE;
  TOUCH(event);
}


static int
hex_get_largest_hex_index(GtkHex *hex)
{
  int full_lines;
  int offset;

  if (!hex)
    return 0;

  full_lines = hex->data_size / HEX_LINE_BYTES;
  offset = hex->data_size % HEX_LINE_BYTES;

  return full_lines * HEX_LINE_WIDTH + HEX_LINE_START + 1 + (offset - 1 ) * 3 + (offset > 8 ? 1 : 0);
}


static int
hex_get_largest_ascii_index(GtkHex *hex)
{
  int full_lines;
  int offset;

  if (!hex)
    return 0;

  full_lines = hex->data_size / HEX_LINE_BYTES;
  offset = hex->data_size % HEX_LINE_BYTES;
  
  return full_lines * HEX_LINE_WIDTH + HEX_LINE_START_ASCII + offset - 1 + (offset > 8 ? 1 : 0);
}


/* Assuming the cursor would be placed at INDEX, is that
 * a feasible (i.e. hex or ascii, but not whitespace) index?
 * If so, the same index is returned,
 * otherwise the nearest one that actually contains
 * non-whitespace.
 */
static int
hex_get_nowhite_index(GtkHex *hex, int index)
{
  GtkEditable *ed;
  int line_offset, last_line, line;
  int offset_hex_max, offset_ascii_max;

  if (!hex)
    return FALSE;

  ed = GTK_EDITABLE(hex->hex_text);

  if (hex->mode == GTK_RAW_ASCII)
    return index;

  line_offset = index % HEX_LINE_WIDTH;

  offset_hex_max   = hex_get_largest_hex_index(hex) % HEX_LINE_WIDTH;
  offset_ascii_max = hex_get_largest_ascii_index(hex) % HEX_LINE_WIDTH;  

  line = index / HEX_LINE_WIDTH;
  last_line = hex_get_largest_hex_index(hex) / HEX_LINE_WIDTH;

  if (line_offset < HEX_LINE_START)
    {
      index += HEX_LINE_START - (line_offset % HEX_LINE_START);
      return index;
    }

  /* Below, we need to check if there are actually enough bytes in the
   * requested line -- it could be that we're in the last row
   * where we may have less!
   */

  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_END)
    {
      if (line == last_line)
	{
	  line_offset = MIN(line_offset, offset_hex_max);
	  index = (index / HEX_LINE_WIDTH) * HEX_LINE_WIDTH + line_offset;
	}

      if (line_offset <= HEX_LINE_LEFT_MIDDLE)
	{
	  if ((line_offset - HEX_LINE_START) % 3 == 2)
	    index--;

	  return index;
	}

      if (line_offset >= HEX_LINE_RIGHT_MIDDLE)
	{
	  if ((line_offset - HEX_LINE_RIGHT_MIDDLE) % 3 == 2)
	    index--;

	  return index;
	}

      return index - ((line_offset - HEX_LINE_START) % (HEX_BLOCK_LEN-1));
    }
  
  if (line_offset > HEX_LINE_END && line_offset < HEX_LINE_START_ASCII)
    {
      if (line == last_line)
	line_offset = MIN(line_offset, offset_hex_max);

      index = (index / HEX_LINE_WIDTH) * HEX_LINE_WIDTH + line_offset;

      return index;
    }

  return MIN(index, hex_get_largest_ascii_index(hex));
}


/* Returns TRUE when the editor is in hex mode and the
 * currently selected character is in the hex area.
 */
static gboolean
hex_cursor_over_hex(GtkHex *hex)
{
  GtkEditable *ed;
  int line_offset;

  if (!hex || hex->mode == GTK_RAW_ASCII)
    return FALSE;

  ed = GTK_EDITABLE(hex->hex_text);
  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;

  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_END)
    return TRUE;

  return FALSE;
}


/* Returns TRUE when the editor is in hex mode and the
 * currently selected character is in the ascii area.
 */
static gboolean
hex_cursor_over_hex_ascii(GtkHex *hex)
{
  GtkEditable *ed;
  int line_offset;

  if (!hex || hex->mode == GTK_RAW_ASCII)
    return FALSE;

  ed = GTK_EDITABLE(hex->hex_text);
  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;

  if (line_offset >= HEX_LINE_START_ASCII)
    return TRUE;
  
  return FALSE;
}


/* Returns TRUE when the editor is in hex mode and the
 * high nibble (i.e. left character) of a byte is selected,
 * FALSE otherwise.
 */
static gboolean  
hex_cursor_over_high_nibble(GtkHex *hex)
{
  GtkEditable *ed;
  int line_offset;

  if (!hex || hex->mode == GTK_RAW_ASCII)
    return FALSE;

  ed = GTK_EDITABLE(hex->hex_text);
  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;
  
  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_LEFT_MIDDLE)
    {
      if ((line_offset - HEX_LINE_START) % 3 == 0)
	return TRUE;      
    }

  if (line_offset >= HEX_LINE_RIGHT_MIDDLE && line_offset <= HEX_LINE_END)
    {
      if ((line_offset - HEX_LINE_RIGHT_MIDDLE) % 3 == 0)
	return TRUE;      
    }
  
  return FALSE;
}


/* Calculates and returns the currently selected byte in the
 * data chunk being displayed from the selected character in
 * either hex or ascii mode. Blood, sweat & tears.
 */
static int
hex_cursor_index_to_byte_index(GtkHex *hex)
{
  GtkEditable *ed = GTK_EDITABLE(hex->hex_text);
  int line_num;
  int line_offset;

  if (!hex || hex->mode == GTK_RAW_ASCII)
    return -1;

  ed = GTK_EDITABLE(hex->hex_text);

  line_num = ed->selection_start_pos / HEX_LINE_WIDTH;
  line_offset = ed->selection_start_pos % HEX_LINE_WIDTH;
  
  if (line_offset >= HEX_LINE_START && line_offset <= HEX_LINE_END)
    {
      if (line_offset <= HEX_LINE_LEFT_MIDDLE)
	return line_num * 16 + (line_offset - HEX_LINE_START) / 3;
      else
	return line_num * 16 + 8 + (line_offset - HEX_LINE_RIGHT_MIDDLE) / 3;
    }

  if (line_offset >= HEX_LINE_START_RIGHT_ASCII)
    return line_num * 16 + 8 + (line_offset - HEX_LINE_START_RIGHT_ASCII);

  if (line_offset >= HEX_LINE_START_ASCII)
    return line_num * 16 + (line_offset - HEX_LINE_START_ASCII);
  
  return -1;
}


static void
hex_byte_index_to_cursor_indices(int byte_index, int *i1, int *i2)
{
  int line_num, line_offset;

  if (!i1 || !i2 || byte_index < 0) 
    return;

  line_num = byte_index / 16;
  line_offset = byte_index % 16;

  (*i1) =
    line_num * HEX_LINE_WIDTH +
    HEX_LINE_START + line_offset * 3 + (line_offset >= 8 ? 1 : 0);

  (*i2) =
    line_num * HEX_LINE_WIDTH +
    HEX_LINE_START_ASCII + line_offset + (line_offset >= 8 ? 1 : 0);  
}


/* The heart of the editor -- creates the hex editor hex-mode
 * string from the input data and returns it. The string must
 * be freed when it's not needed any more.
 */
static guchar *
hex_get_hex_text(const guchar *data, int data_size)
{
  int x, y, num_lines, len;
  guchar      *hex_data;
  const guchar *data_ptr;
  guchar      *hex_data_ptr;
  guchar      *ascii_ptr;
  char         hex_byte[3];
  char         offset[5];
  guchar       ascii_byte;
	
  if (!data)
    return NULL;

  num_lines = (data_size / 16) + 1;
  len = num_lines * HEX_LINE_WIDTH;
  if (! (hex_data = g_new0(guchar, len + 1)))
    return NULL;

  memset(hex_data, ' ', len);
	
  hex_data_ptr = hex_data;
  ascii_ptr = hex_data_ptr + 50;
  x = y = 0;
	
  for (data_ptr = data; data_ptr < data + data_size; data_ptr++)
    {
      if (x == 0)
	{
	  g_snprintf(offset, 5, "%.4x", data_ptr - data);
	  memcpy(hex_data_ptr, offset, 4);
	  hex_data_ptr += 6;
	  ascii_ptr = hex_data_ptr + 50;
	}
	    
      g_snprintf(hex_byte, 3, "%.2x", (guchar) *data_ptr);

      /* Workaround for a nonsense gcc warning -- char's range is
	 obviously not limited to 128 ... */
	    
      {
	int val = (guchar) *data_ptr;

	if (hex_is_char_unprintable(val))
	  {
	    if (val == 0)
	      ascii_byte = NULL_CHAR;
	    else
	      ascii_byte = NONPRINT_CHAR;
	  }
	else
	  {
	    ascii_byte = val;
	  }
      }
	    
      *hex_data_ptr++ = hex_byte[0];
      *hex_data_ptr++ = hex_byte[1];
      *hex_data_ptr++ = ' ';
      *ascii_ptr++    = ascii_byte;
	    
      if (x == 7)
	{
	  *hex_data_ptr++ = ' ';
	  *ascii_ptr++ = ' ';
	}
	    
      x++;
	    
      if (x == 16)
	{
	  x = 0;
	  *ascii_ptr++ = '\n';
	  hex_data_ptr = ascii_ptr;
	}
    }
  
  return hex_data;
}


/* This one creates and returns the ascii version of
 * the data being displayed. Zeroes and other non-displayable
 * characters in the input are substituted with dots.
 * 
 * The string is returned through the result pointer.
 * An additional string will be returned through null_map,
 * that contains a null character "0" wherever the original text
 * contained a null byte, and whitespace everywhere else.
 * The strings must be freed when it's no longer needed.
 */
void
hex_get_ascii_text(const guchar *data, int data_size,
		   guchar **result, guchar **null_map)
{
  guchar *result_ptr;
  const guchar *data_ptr;
  guchar *null_map_ptr;

  if (!data || !result || !null_map)
    return;

  *result = result_ptr = g_new0(guchar, data_size + 1);
  *null_map = null_map_ptr = g_new0(guchar, data_size + 1);
  memset(null_map_ptr, ' ', data_size);

  if (!result || !null_map)
    {
      g_free(result);
      g_free(null_map);
      return;
    }

  for (data_ptr = data; data_ptr < data + data_size; result_ptr++, data_ptr++, null_map_ptr++)
    {
      char val = *data_ptr;

      if (hex_is_char_unprintable(val))
	{
	  switch (val)
	    {
	    case '\n':
	      *result_ptr = *data_ptr;
	      *null_map_ptr = '\n';
	      break;

	    case 0:
	      *result_ptr = NULL_CHAR;
	      *null_map_ptr = '0';
	      break;

	    default:
	      *result_ptr = NONPRINT_CHAR;
	      *null_map_ptr = *data_ptr;
	    }
	}
      else
	{
	  *result_ptr = *data_ptr;
	}
    }
}


static void      
hex_sync_data_to_ascii(GtkHex *hex)
{
  guint len, i;
  gchar *text;

  if (!hex || hex->mode != GTK_RAW_ASCII)
    return;

  text = gtk_editable_get_chars(GTK_EDITABLE(hex->hex_text), 0, -1);
  len = strlen(text);

  memset(hex->data, 0, hex->data_size);
  memcpy(hex->data, text, MIN(len, hex->data_size));
  g_free(text);

  text = hex->priv->null_map;

  for (i = 0; i < MIN(len, hex->data_size); i++)
    {
      if (text[i] != ' ')
	{
	  if (text[i] == '0')	    
	    hex->data[i] = 0;
	  else
	    hex->data[i] = text[i];
	}
    }
}


static void
gtk_hex_destroy(GtkObject *object)
{
  GtkHex *hex;

  g_return_if_fail(object != NULL);
  g_return_if_fail(GTK_IS_HEX(object));

  hex = GTK_HEX(object);

  g_free(hex->priv->null_map);
  g_free(hex->priv);
  
  if (GTK_OBJECT_CLASS(parent_class)->destroy)
    (* GTK_OBJECT_CLASS(parent_class)->destroy) (object);
}


static void
gtk_hex_class_init (GtkHexClass *class)
{
  GtkObjectClass *object_class;
  GtkVBoxClass   *vbox_class;

  object_class = (GtkObjectClass *) class;
  vbox_class   = (GtkVBoxClass *) class;

  parent_class = gtk_type_class(gtk_vbox_get_type());

  hex_signals[CHANGED] =
    gtk_signal_new ("changed",
		    GTK_RUN_LAST,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GtkHexClass, changed),
		    gtk_marshal_NONE__INT_INT,
		    GTK_TYPE_NONE, 2,
		    GTK_TYPE_INT,
		    GTK_TYPE_INT);
  
  hex_signals[MOVE_CURSOR] =
    gtk_signal_new ("move_cursor",
		    GTK_RUN_LAST | GTK_RUN_ACTION,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GtkHexClass, move_cursor),
		    gtk_marshal_NONE__INT,
		    GTK_TYPE_NONE, 1, 
		    GTK_TYPE_INT);

  hex_signals[SELECTION] =
    gtk_signal_new ("selection",
		    GTK_RUN_LAST | GTK_RUN_ACTION,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GtkHexClass, selection),
		    gtk_marshal_NONE__STRING,
		    GTK_TYPE_NONE, 1,
		    GTK_TYPE_STRING);

  gtk_object_class_add_signals(object_class, hex_signals, LAST_SIGNAL);

  object_class->destroy = gtk_hex_destroy;

  class->changed = NULL;
  class->move_cursor = NULL;
}


static void
gtk_hex_init (GtkHex *hex)
{
  GtkHexPrivate *priv;
  GtkWidget *mode_hbox, *hex_vbox;
  GtkWidget *scrolledwin;
  GSList    *mode_hbox_group = NULL;
   
  if (!hex)
    return;

  hex_vbox = gtk_vbox_new (FALSE, 0);
  gtk_widget_ref (hex_vbox);
  gtk_object_set_data_full (GTK_OBJECT (hex), "hex_vbox", hex_vbox,
			    (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (hex_vbox);
  gtk_container_add (GTK_CONTAINER (hex), hex_vbox);

  mode_hbox = gtk_hbox_new (FALSE, 0);
  gtk_widget_ref (mode_hbox);
  gtk_object_set_data_full (GTK_OBJECT (hex_vbox), "mode_hbox", mode_hbox,
                            (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (mode_hbox);
  gtk_box_pack_start (GTK_BOX (hex_vbox), mode_hbox, FALSE, FALSE, 0);

  hex->mode_button_hex = gtk_radio_button_new_with_label (mode_hbox_group, _("Hex/ASCII"));
  mode_hbox_group = gtk_radio_button_group (GTK_RADIO_BUTTON (hex->mode_button_hex));
  gtk_widget_ref (hex->mode_button_hex);
  gtk_object_set_data_full (GTK_OBJECT (hex_vbox), "mode_button_hex", hex->mode_button_hex,
                            (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (hex->mode_button_hex);
  gtk_box_pack_start (GTK_BOX (mode_hbox), hex->mode_button_hex, FALSE, FALSE, 0);

  hex->mode_button_ascii = gtk_radio_button_new_with_label (mode_hbox_group, _("ASCII only"));
  mode_hbox_group = gtk_radio_button_group (GTK_RADIO_BUTTON (hex->mode_button_ascii));
  gtk_widget_ref (hex->mode_button_ascii);
  gtk_object_set_data_full (GTK_OBJECT (hex_vbox), "mode_button_ascii", hex->mode_button_ascii,
                            (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (hex->mode_button_ascii);
  gtk_box_pack_start (GTK_BOX (mode_hbox), hex->mode_button_ascii, FALSE, FALSE, 0);
  scrolledwin = gtk_scrolled_window_new (NULL, NULL);
  gtk_widget_ref (scrolledwin);
  gtk_object_set_data_full (GTK_OBJECT (hex_vbox), "scrolledwin", scrolledwin,
                            (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (scrolledwin);
  gtk_box_pack_start (GTK_BOX (hex_vbox), scrolledwin, TRUE, TRUE, 0);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);

  hex->hex_text = gtk_text_new (NULL, NULL);
  gtk_widget_ref (hex->hex_text);
  gtk_object_set_data_full (GTK_OBJECT (hex_vbox), "hex_text", hex->hex_text,
                            (GtkDestroyNotify) gtk_widget_unref);
  gtk_widget_show (hex->hex_text);
  gtk_container_add (GTK_CONTAINER (scrolledwin), hex->hex_text);

  gtk_text_set_editable(GTK_TEXT(hex->hex_text), FALSE);
  gtk_text_set_word_wrap(GTK_TEXT(hex->hex_text), FALSE);
  gtk_text_set_line_wrap(GTK_TEXT(hex->hex_text), FALSE);

  hex->mode = GTK_RAW_HEX;
  
  priv = g_new0(GtkHexPrivate, 1);
  priv->null_map = g_new0(gchar, 1024);
  priv->null_map_used = 0;
  priv->null_map_len  = 1024;
  hex->priv = priv;

  gtk_signal_connect (GTK_OBJECT (hex->mode_button_ascii), "toggled",
                      GTK_SIGNAL_FUNC (on_mode_ascii_toggled),
                      hex);

  gtk_signal_connect (GTK_OBJECT (hex->mode_button_hex), "toggled",
                      GTK_SIGNAL_FUNC (on_mode_hex_toggled),
                      hex);

  gtk_signal_connect (GTK_OBJECT (hex->hex_text), "key_press_event",
                      GTK_SIGNAL_FUNC (on_hex_text_key_press_event),
                      hex);

  gtk_signal_connect (GTK_OBJECT (hex->hex_text), "motion_notify_event",
		      GTK_SIGNAL_FUNC (on_hex_text_motion_notify_event),
		      hex);

  gtk_signal_connect_after (GTK_OBJECT (hex->hex_text), "button_press_event",
			    GTK_SIGNAL_FUNC (on_hex_button_press_event),
			    hex);

  gtk_signal_connect(GTK_OBJECT (hex->hex_text), "insert_text",
			    GTK_SIGNAL_FUNC (on_hex_text_inserted),
			    hex);

  gtk_signal_connect_after (GTK_OBJECT (hex->hex_text), "insert_text",
			    GTK_SIGNAL_FUNC (on_hex_text_inserted_after),
			    hex);
  
  gtk_signal_connect_after (GTK_OBJECT (hex->hex_text), "delete_text",
			    GTK_SIGNAL_FUNC (on_hex_text_deleted),
			    hex);
}


guint
gtk_hex_get_type ()
{
  static guint gtk_hex_type = 0;

  if (!gtk_hex_type)
    {
      GtkTypeInfo gtk_hex_info =
      {
        "GtkHex",
        sizeof (GtkHex),
        sizeof (GtkHexClass),
        (GtkClassInitFunc) gtk_hex_class_init,
        (GtkObjectInitFunc) gtk_hex_init,
	/* reserved_1 */ NULL,
        /* reserved_2 */ NULL,
        (GtkClassInitFunc) NULL,
      };

      gtk_hex_type = gtk_type_unique (gtk_vbox_get_type (), &gtk_hex_info);
    }

  return gtk_hex_type;
}


GtkWidget *
gtk_hex_new(void)
{
  return GTK_WIDGET(gtk_type_new(gtk_hex_get_type()));
}


void       
gtk_hex_set_content(GtkHex *hex, guchar *data, int data_size)
{
  guchar     *hex_text = NULL;
  guchar     *null_map_text = NULL;

  if (!hex || !data || !data_size)
    return;

  switch (hex->mode)
    {
    case GTK_RAW_HEX:
      hex_text = hex_get_hex_text((guchar *) data, data_size);
      if (!hex_text)
	goto cleanup;
      break;

    case GTK_RAW_ASCII:
      hex_get_ascii_text((guchar *) data, data_size, &hex_text, &null_map_text);
      if (!hex_text || !null_map_text)
	goto cleanup;

      null_map_delete(hex->priv, 0, -1);
      null_map_insert(hex->priv, (char *) null_map_text, 0, strlen((char *) null_map_text));
      break;
      
    default:
      goto cleanup;
    }
  
  gtk_text_freeze(GTK_TEXT(hex->hex_text));
  gtk_signal_handler_block_by_data(GTK_OBJECT(hex->hex_text), hex);

  gtk_editable_delete_text(GTK_EDITABLE(hex->hex_text), 0, -1);
  gtk_text_insert(GTK_TEXT(hex->hex_text), NULL, NULL, NULL,
		  (char *) hex_text, strlen((char *) hex_text));

  gtk_signal_handler_unblock_by_data(GTK_OBJECT(hex->hex_text), hex);
  gtk_text_thaw(GTK_TEXT(hex->hex_text));

  /* Old data isn't freed -- we don't possess it! */
  hex->data = (guchar *) data;
  hex->data_size = data_size;

 cleanup:
  g_free(hex_text);
  g_free(null_map_text);
}


void       
gtk_hex_set_mode(GtkHex *hex, GtkRawMode mode)
{
  if (!hex)
    return;
  
  hex->mode = mode;

  switch(mode)
    {
    case GTK_RAW_ASCII:
      gtk_text_set_line_wrap(GTK_TEXT(hex->hex_text), TRUE);
      gtk_text_set_editable(GTK_TEXT(hex->hex_text), TRUE);
      break;

    case GTK_RAW_HEX:
      gtk_text_set_line_wrap(GTK_TEXT(hex->hex_text), FALSE);
      gtk_text_set_editable(GTK_TEXT(hex->hex_text), FALSE);
      break;

    default:
      g_assert_not_reached();
    }
  
  gtk_hex_set_content(hex, hex->data, hex->data_size);
}
